﻿module BoardTool.Tool

open System
open System.Collections.Generic

open BoardTool.DirTree

type IHelpFormatter =
    abstract append : string -> unit
    abstract appendCommandHelp : string * string -> unit
    abstract appendArgumentHelp : string * string -> unit
    abstract printHelp : IO.TextWriter -> unit

type ICommand =
    abstract id : string
    abstract run : unit -> unit
    
type ICommandFactory =
    abstract name : string
    abstract appendBriefHelp : IHelpFormatter -> unit
    abstract appendDetailedHelp : IHelpFormatter -> unit
    abstract parse : string list -> ICommand * string list

type private HelpEntry =
    | Str of string
    | Cmd of string * string
    | Arg of string * string

type Tool() =
    let cfsByName = Dictionary<string, ICommandFactory>()
    let cfs = ResizeArray<ICommandFactory>()

    let helpFormatter() =
        let maxCmdLength = ref 0
        let maxArgLength = ref 0
        let entries = ResizeArray<HelpEntry>()
        { new IHelpFormatter with
            member this.append( str ) =
                entries.Add( Str str )
            member this.appendCommandHelp( cmdSyntax, cmdHelp ) =
                entries.Add( Cmd( cmdSyntax, cmdHelp ) )
                maxCmdLength := max !maxCmdLength cmdSyntax.Length
            member this.appendArgumentHelp( argSyntax, argHelp ) =
                entries.Add( Arg( argSyntax, argHelp ) )
                maxArgLength := max !maxArgLength argSyntax.Length
            member this.printHelp( wr ) =
                let padding = max !maxCmdLength (!maxArgLength)
                for he in entries do
                    match he with
                    | Str str -> 
                        fprintfn wr "%s" str
                    | Cmd( cmdSyntax, cmdHelp ) ->
                        let cmdSyntax = cmdSyntax.PadRight( padding )
                        fprintfn wr "%s   %s" cmdSyntax cmdHelp
                    | Arg( argSyntax, argHelp ) ->
                        let argSyntax = argSyntax.PadRight( padding )
                        fprintfn wr "  %s %s" argSyntax argHelp
                entries.Clear()
        }

    member this.add( cf ) =
        cfs.Add( cf )
        cfsByName.[cf.name] <- cf

    member this.printBriefHelp( wr : IO.TextWriter ) =
        let hf = helpFormatter()
        for cf in cfs do    
            cf.appendBriefHelp( hf )
        hf.printHelp( wr )

    member this.printDetailedHelp( wr : IO.TextWriter ) =
        let hf = helpFormatter()
        for cf in cfs do
            cf.appendDetailedHelp( hf )
            hf.append( "" )
        hf.printHelp( wr )

    member this.printCommandHelp( cmd : string, wr : IO.TextWriter ) =
        let pp, cf = cfsByName.TryGetValue( cmd )
        if pp then
            let hf = helpFormatter()
            cf.appendDetailedHelp( hf )
            hf.printHelp( wr )
        else
            failwithf "Unknown command: %s" cmd

    member this.run( args : string[] ) =
        let rec runRec (args : string list) =
            match args with
            | cmdName :: args' ->
                let pp, cf = cfsByName.TryGetValue( cmdName )
                if pp then
                    let cmd, args' = cf.parse( args' )
                    let stopwatch = Diagnostics.Stopwatch.StartNew()
                    cmd.run()
                    stopwatch.Stop()
                    eprintfn "%s: done, time taken: %O" cmd.id stopwatch.Elapsed
                    runRec args'                    
                else
                    failwithf "Unknown command: %s, stopped." cmdName
            | [] -> ()
        runRec <| Array.toList args


type HelpCommandFactory( tool : Tool ) =
    let printBriefHelp =
        { new ICommand with
            member this.id = "(help)"
            member this.run() = tool.printBriefHelp( Console.Out )
        }

    let printDetailedHelp =
        { new ICommand with
            member this.id = "(help *)"
            member this.run() = tool.printDetailedHelp( Console.Out )
        }

    let printCommandHelp cmd = 
        { new ICommand with
            member this.id = "(help " + cmd + ")"
            member this.run() = tool.printCommandHelp( cmd, Console.Out )
        }

    interface ICommandFactory with
        member this.name = "help"
        member this.appendBriefHelp( hf ) =
            hf.appendCommandHelp( "help [CMD]", "displays help page" )            
        member this.appendDetailedHelp( hf ) =
            hf.appendCommandHelp( "help [CMD]", "displays help page" )
            hf.appendArgumentHelp( "(no args)", "brief description of all available commands" )
            hf.appendArgumentHelp( "CMD", "detailed description of CMD" )
            hf.appendArgumentHelp( "*", "detailed description of all available commands" )
        member this.parse( args : string list ) =
            match args with
            | [] -> 
                printBriefHelp, []
            | "*" :: args' ->
                printDetailedHelp, args'
            | cmd :: args' ->
                printCommandHelp cmd, args'

